當一群相似結構的物件們,在執行相同方法時卻有著不同實作內容,那可以將方法封裝成獨立物件。當需要增加新的方法時,不用改變物件的結構,只需要增加封裝的方法就能使用。如此一來,讓方法的擴充、修改變得容易許多。
試想一個 RPG 的情境,建立一個職業物件,本身可以執行兩個方法:
class Warrior {
  constructor() {
    this.hp = 35
    this.mp = 0
  }
  attack() {
    // 執行 Warrior 專屬的 attack
  }
  defense() {
    // 執行 Warrior 專屬的 attack
  }
}
現在,要建立一個擁有相同方法的新職業,其方法內的實作細節不同:
class Thief {
  constructor() {
    this.hp = 30
    this.mp = 0
  }
  attack() {
    // 執行 Thief 專屬的 attack
  }
  defense() {
    // 執行 Thief 專屬的 defense
  }
}
假如又需要新增一個職業,同時三個職業還要新增一個方法呢?
class Warrior {
  constructor() {
    this.hp = 35
    this.mp = 0
  }
  attack() {
    // 執行 Warrior 專屬的 attack
  }
  defense() {
    // 執行 Warrior 專屬的 defense
  }
  run() {
    // 執行 Warrior 專屬的 run
  }
}
class Thief {
  constructor() {
    this.hp = 30
    this.mp = 0
  }
  attack() {
    // 執行 Thief 專屬的 attack
  }
  defense() {
    // 執行 Thief 專屬的 defense
  }
  run() {
    // 執行 Thief 專屬的 run
  }
}
class BlackMage {
  constructor() {
    this.hp = 25
    this.mp = 10
  }
  attack() {
    // 執行 BlackMage 專屬的 attack
  }
  defense() {
    // 執行 BlackMage 專屬的 defense
  }
  run() {
    // 執行 BlackMage 專屬的 run
  }
}
隨著職業的增加、方法的增加,會變得越難管理。
仔細觀察,職業的結構相似,差異在方法,那有沒有一個模式,可以將方法封裝起來,同時使用方法時,能夠配合不同的職業而執行不同的內容呢?
這就是 Visitor 模式的由來。
作法是:
剛剛的 RPG 情境,發展到現在,可以製作成表格:
| Warrior | Thief | BlackMage | |
|---|---|---|---|
| attack | AttackByWarrior | AttackByThief | AttackByBlackMage | 
| defense | DefenseByWarrior | DefenseByThief | DefenseByBlackMage | 
| run | RunByWarrior | RunByThief | RunByBlackMage | 
| magic | MagicByWarrior | MagicByThief | MagicByBlackMage | 
以下範例以「模擬簡易 RPG」為核心,將製作:
建立方法的虛擬層親代物件:Action
public interface Action {
    public abstract void executeWarriorAction(Warrior element);
    public abstract void executeThiefAction(Thief element);
    public abstract void executeBlackMageAction(BlackMage element);
}
建立職業物件的虛擬層親代:Character
public abstract class Character {
    protected String name;
    protected String job;
    protected int hp;
    protected int mp;
    protected int level;
    protected Character(String name, String job, int hp, int mp, int level) {
        this.name = name;
        this.job = job;
        this.hp = hp;
        this.mp = mp;
        this.level = level;
        System.out.println("The character " + this.name + " is created successfully");
    }
    public abstract void levelUp();
    public abstract void showCharacterInformation();
    public abstract void accept(Action visitor);
}
建立職業物件的子代:Warrior、Thief、BlackMage(Element 物件)
public class Warrior extends Character {
    public Warrior(String name) {
        super(name, "Warrior", 30, 0, 1);
    }
    @Override
    public void showCharacterInformation() {
        System.out.println("The class is " + job + ", and the name is " + name);
        System.out.println("The hp is " + hp + ", the mp is: " + mp + "and the level is " + level);
        System.out.println("'I see, I come, I conquer' by Julius Caesar\n");
    }
    @Override
    public void accept(Action visitor) {
        visitor.executeWarriorAction(this);
    }
    @Override
    public void levelUp() {
        this.hp += 3;
        this.level += 1;
    }
}
public class Thief extends Character {
    public Thief(String name) {
        super(name, "Thief", 35, 0, 1);
    }
    @Override
    public void showCharacterInformation() {
        System.out.println("The class is " + job + ", and the name is " + name);
        System.out.println("The hp is " + hp + ", the mp is: " + mp + "and the level is " + level);
        System.out.println("'Everything is permitted, Nothing is true.' by Assassin's Creed\n");
    }
    @Override
    public void accept(Action visitor) {
        visitor.executeThiefAction(this);
    }
    @Override
    public void levelUp() {
        this.hp += 2;
        this.level += 1;
    }
}
public class BlackMage extends Character {
    public BlackMage(String name) {
        super(name, "Black Mage", 35, 0, 1);
    }
    @Override
    public void showCharacterInformation() {
        System.out.println("The class is " + job + ", and the name is " + name);
        System.out.println("The hp is " + hp + ", the mp is: " + mp + "and the level is " + level);
        System.out.println("'Knowledge is power, but using it wisely is the key.' by Khadgar\n");
    }
    @Override
    public void accept(Action visitor) {
        visitor.executeBlackMageAction(this);
    }
    @Override
    public void levelUp() {
        this.hp += 1;
        this.mp += 3;
        this.level += 1;
    }
}
建立方法的子代物件:Attack、Defense、Run、Magic(Visitor 物件)
public class Attack implements Action {
    @Override
    public void executeWarriorAction(Warrior element) {
        System.out.println("Use sword to attack enemy");
    }
    @Override
    public void executeThiefAction(Thief element) {
        System.out.println("Use dagger to stab enemy");
    }
    @Override
    public void executeBlackMageAction(BlackMage element) {
        System.out.println("It's is wrong decision");
    }
}
public class Defense implements Action {
    @Override
    public void executeWarriorAction(Warrior element) {
        System.out.println("Use shield to protect team members");
    }
    @Override
    public void executeThiefAction(Thief element) {
        System.out.println("Try to dodge this attack");
    }
    @Override
    public void executeBlackMageAction(BlackMage element) {
        System.out.println("Nothing to do");
    }
}
public class Run implements Action {
    @Override
    public void executeWarriorAction(Warrior element) {
        System.out.println("The last one to run");
    }
    @Override
    public void executeThiefAction(Thief element) {
        System.out.println("The fast one to run");
    }
    @Override
    public void executeBlackMageAction(BlackMage element) {
        System.out.println("The slow one to run");
    }
}
public class Magic implements Action {
    @Override
    public void executeWarriorAction(Warrior element) {
        System.out.println("There is no mana");
    }
    @Override
    public void executeThiefAction(Thief element) {
        System.out.println("The is no mana");
    }
    @Override
    public void executeBlackMageAction(BlackMage element) {
        System.out.println("Use fire ball!!!");
    }
}
測試,模擬 RPG 的角色同時執行相同的命令:RPGVisitorSample
public class RPGVisitorSample {
    public static void main(String[] args) {
        System.out.println("建立角色");
        List<Character> characters = new ArrayList<>();
        characters.add(new Warrior("Zest"));
        characters.add(new Thief("Sauber"));
        characters.add(new BlackMage("Fritz"));
        System.out.println("\n集體攻擊");
        for (Character character : characters) {
            character.accept(new Attack());
        }
        System.out.println("\n集體防禦");
        for (Character character : characters) {
            character.accept(new Defense());
        }
        System.out.println("\n集體使用魔法");
        for (Character character : characters) {
            character.accept(new Magic());
        }
        System.out.println("\n集體逃跑");
        for (Character character : characters) {
            character.accept(new Run());
        }
    }
}
建立方法的虛擬層親代物件:Action
/** @abstract */
class Character {
  /**
   * @param {string} name
   * @param {string} job
   * @param {int} hp
   * @param {int} mp
   * @param {int} level
   */
  constructor(name, job, hp, mp, level) {
    this.name = name;
    this.job = job;
    this.hp = hp;
    this.mp = mp;
    this.level = level;
    console.log("The character " + this.name + " is created successfully");
  }
  /** @abstract */
  levelUp() { return; }
  /** @abstract */
  showCharacterInformation() { return; }
  /**
   * @abstract
   * @param {Action} visitor
   */
  accept(visitor) { return; }
}
建立職業物件的虛擬層親代:Character
/** @abstract */
class Action {
  /**
   * @abstract
   * @param {Warrior} element
   */
  executeWarriorAction(element) { return; }
  /**
   * @abstract
   * @param {Thief} element
   */
  executeThiefAction(element) { return; }
  /**
   * @abstract
   * @param {BlackMage} element
   */
  executeBlackMageAction(element) { return; }
}
建立職業物件的子代:Warrior、Thief、BlackMage(Element 物件)
class Warrior extends Character {
  /** @param {string} name */
  constructor(name) {
    super(name, "Warrior", 30, 0, 1);
  }
  /** @override */
  showCharacterInformation() {
    console.log("The class is " + this.job + ", and the name is " + this.name);
    console.log("The hp is " + this.hp + ", the mp is: " + this.mp + "and the level is " + this.level);
    console.log("'I see, I come, I conquer' by Julius Caesar\n");
  }
  /**
   * @override
   * @param {Action} visitor
   */
  accept(visitor) {
    visitor.executeWarriorAction(this);
  }
  /** @override */
  levelUp() {
    this.hp += 3;
    this.level += 1;
  }
}
class Thief extends Character {
  /** @param {string} name */
  constructor(name) {
    super(name, "Thief", 35, 0, 1);
  }
  /** @override */
  showCharacterInformation() {
    console.log("The class is " + this.job + ", and the name is " + this.name);
    console.log("The hp is " + this.hp + ", the mp is: " + this.mp + "and the level is " + this.level);
    console.log("'Everything is permitted, Nothing is true.' by Assassin's Creed\n");
  }
  /**
   * @override
   * @param {Action} visitor
   */
  accept(visitor) {
    visitor.executeThiefAction(this);
  }
  /** @override */
  levelUp() {
    this.hp += 2;
    this.level += 1;
  }
}
class BlackMage extends Character {
  /** @param {string} name */
  constructor(name) {
    super(name, "Black Mage", 35, 0, 1);
  }
  /** @override */
  showCharacterInformation() {
    console.log("The class is " + this.job + ", and the name is " + this.name);
    console.log("The hp is " + this.hp + ", the mp is: " + this.mp + "and the level is " + this.level);
    console.log("'Knowledge is power, but using it wisely is the key.' by Khadgar\n");
  }
  /**
   * @override
   * @param {Action} visitor
   */
  accept(visitor) {
    visitor.executeBlackMageAction(this);
  }
  /** @override */
  levelUp() {
    this.hp += 1;
    this.mp += 3;
    this.level += 1;
  }
}
建立方法的子代物件:Attack、Defense、Run、Magic(Visitor 物件)
class Attack extends Action {
  /**
   * @override
   * @param {Warrior} element
   */
  executeWarriorAction(element) {
    console.log("Use sword to attack enemy");
  }
  /**
   * @override
   * @param {Thief} element
   */
  executeThiefAction(element) {
    console.log("Use dagger to stab enemy");
  }
  /**
   * @override
   * @param {BlackMage} element
   */
  executeBlackMageAction(element) {
    console.log("It's is wrong decision");
  }
}
class Defense extends Action {
  /**
   * @override
   * @param {Warrior} element
   */
  executeWarriorAction(element) {
    console.log("Use shield to protect team members");
  }
  /**
   * @override
   * @param {Thief} element
   */
  executeThiefAction(element) {
    console.log("Try to dodge this attack");
  }
  /**
   * @override
   * @param {BlackMage} element
   */
  executeBlackMageAction(element) {
    console.log("Nothing to do");
  }
}
class Run extends Action {
  /**
   * @override
   * @param {Warrior} element
   */
  executeWarriorAction(element) {
    console.log("The last one to run");
  }
  /**
   * @override
   * @param {Thief} element
   */
  executeThiefAction(element) {
    console.log("The fast one to run");
  }
  /**
   * @override
   * @param {BlackMage} element
   */
  executeBlackMageAction(element) {
    console.log("The slow one to run");
  }
}
class Magic extends Action {
  /**
   * @override
   * @param {Warrior} element
   */
  executeWarriorAction(element) {
    console.log("There is no mana");
  }
  /**
   * @override
   * @param {Thief} element
   */
  executeThiefAction(element) {
    console.log("The is no mana");
  }
  /**
   * @override
   * @param {BlackMage} element
   */
  executeBlackMageAction(element) {
    console.log("Use fire ball!!!");
  }
}
測試,模擬 RPG 的角色同時執行相同的命令:rpgVisitorSample
const rpgVisitorSample = () => {
  console.log("建立角色");
  /** @type {Character[]} */
  let characters = [];
  characters.push(new Warrior("Zest"));
  characters.push(new Thief("Sauber"));
  characters.push(new BlackMage("Fritz"));
  console.log("\n集體攻擊");
  for (const character of characters) {
    character.accept(new Attack());
  }
  console.log("\n集體防禦");
  for (const character of characters) {
    character.accept(new Defense());
  }
  console.log("\n集體使用魔法");
  for (const character of characters) {
    character.accept(new Magic());
  }
  console.log("\n集體逃跑");
  for (const character of characters) {
    character.accept(new Run());
  }
};
rpgVisitorSample();
Visitor 模式的前提是「多個擁有相同資料結構的物件,在相同名稱的方法內執行不同內容的程式碼」,換句話說,必須在物件的資料結構以及方法都十分清楚時才能套用,這兩個條件缺乏任一都無法使用此方法。
也因為如此,能夠實作的場合不多,當程式碼因為擴充方法而開始混亂時才有登場的機會。
這邊簡單列出模式的優缺點:
最後補充一點,因為 Visitor 與 Element 互相依賴,技術上稱作 Double Dispatch。
所以的模式都介紹完畢,明天將總結各個模式。